package Cammy;
use strict;
use warnings;
# ABSTRACT: a tool to fetch and manipulate webcam images
# VERSION

=head1 SYNOPSIS

    use Cammy;
    Cammy->new(config_file => 'test.conf')->run;

=head1 DESCRIPTION

This project is a collection of scripts intended to be used to
download images and perform basic processing such as cropping,
blurring predefined regions, and adding a basic banner that may
contain text and/or a logo image. L<Image::Magick> is used for
image processing.

Images are retrieved using a given list of URLs that serve
images, more specifically wecbam servers that serve images to
a single page, however it could work with any given URL that
can be downloaded to an image.

=cut

use Cammy::ImageProcessor;
use Cammy::Util qw(verify_config http_client);
use Cammy::Fetch::Weather;
use Config::General qw(ParseConfig);
use File::Path qw/make_path/;
use File::Spec;
use File::Symlink::Atomic;
use Time::HiRes qw(time sleep);
use DateTime;
use DateTime::TimeZone;
use HTTP::Tiny;

=head1 METHODS

=head2 new

The constructor creates a new Cammy object, which you'll call C<< L</run> >> on.

The constructor takes the following key-value pairs:

=over 4

=item config_file

Defaults to F<Cammy.conf>, but you may wish to provide a path pointing
to an alternative.

=item debug

Whether to print debugging information.

=item ua

An L<HTTP::Tiny> object, or compatible.

=back

=cut

sub new {
    my $class = shift;
    my %args  = @_;
    my $self = {
        http        => $args{http} || $args{ua} || http_client(),
        config_file => $args{config_file} || $args{config} || 'Cammy.conf',
        debug       => $args{debug} || 0,
        tz          => DateTime::TimeZone->new( name => 'local' ),
    };
    my %config = ParseConfig($self->{config_file});
    verify_config(\%config, $self->{debug});
    $self->{config} = \%config;

    $self->{max_runtime} = $^T + ($config{cron_interval} * 60);

    _init_webcam_data($self->{config}, $self->{debug}, $self->{tz});

    print __FILE__, " PID: $$\n" if $self->{debug};
    if ($self->{debug}) {
        require Data::Printer;
        Data::Printer->import(
            filters => {
                -external => [qw/DateTime/],
                'DateTime::TimeZone::America::Halifax' => sub { $_[0]->name },
            },
            caller_info => 1,
        );
        p($self);
    }
    return bless $self, $class;
}

# Initialize our data structures
sub _init_webcam_data {
    my $config = shift;
    my $debug  = shift;
    my $tz     = shift;

    my $now_mdy = DateTime->now( time_zone => $tz )->mdy('');
    while (my ($cam, $conf) = each %{ $config->{webcams} }) {
        $conf->{out_dir} = File::Spec->catdir( $config->{out_dir}, $cam, $now_mdy );

        my $report = report_weather($conf);
        $conf->{weather}  = delete $report->{weather};
        $conf->{warnings} = delete $report->{warnings};

        unless( -d $conf->{out_dir} ) {
            print "Creating output directory: $conf->{out_dir}\n";
            make_path($conf->{out_dir});
        }
    }
    if ($debug) {
        require Data::Dumper;
        print '$webcams:', "\n";
        print Data::Dumper::Dumper($config->{webcams});
    }
}

=head2 run

This is the only method you need to call. When configured correctly,
this will orchestrate everything for you.

=cut

sub run {
    my $self = shift;
    my $intervals = (60 * $self->{config}->{cron_interval}) / $self->{config}->{run_interval};

    for (1..$intervals) {
        $self->{start} = time;
        $self->handle_cams();

        my $elapsed = time - delete $self->{start};
        last if $elapsed >= $self->{config}->{run_interval};

        my $sleeptime = 10 - $elapsed;
        warn "Sleeping for $sleeptime\n" if $self->{debug};
        sleep( $sleeptime );
    }
}

=head2 handle_cams

Fork off children to handle each configured webcam.

=cut

sub handle_cams {
    my $self  = shift;

    local $SIG{CHLD} = 'IGNORE'; # auto-reap
    foreach my $cam ( keys %{ $self->{config}->{webcams} }) {
        my $pid = fork();
        if (!defined $pid) {
            die "Couldn't fork: $!"
        }
        elsif ($pid == 0) { # a child
            $self->handle_one_cam($cam);
            exit;
        }
        else { # parent
            print "\n" if $self->{debug};
            last if time - $self->{start} >= $self->{max_runtime};
        }
    }
}

=head2 fetch_image

Downloads the webcam image, returning the binary blob.

On failure, warns and returns false.

=cut

sub fetch_image {
    my $self = shift;
    my $cam  = shift;

    my $http_response = $self->{http}->get( $self->{config}->{webcams}->{$cam}->{image_url} );
    unless ($http_response->{success}) {
        warn <<"END_WARN";
Couldn't download image from $http_response->{url}:
HTTP $http_response->{status} $http_response->{reason}
$http_response->{content}
END_WARN
        return;
    }
    return $http_response->{content};
}

=head2 check_filesize

This checks the size of the webcam image and compares it to the last
one. If the size changes drastically, a warning will be emitted.

=cut

sub check_filesize {
    my $self     = shift;
    my $cam      = shift;
    my $new_size = shift;
    my $webcam   = $self->{config}->{webcams}->{$cam};

    my $old_size = do {
        opendir my $dir, $webcam->{out_dir}
            or die "Couldn't open directory $webcam->{out_dir} for reading: $!";
        my @files =         # Read from the bottom up:
            sort            # 3) Sort by filename (which is a timestamp)
            map {           # 2) Filter out everything but files
                -f File::Spec->catfile($webcam->{out_dir}, $_)
                ? File::Spec->catfile($webcam->{out_dir}, $_)
                : ()
            }
            readdir $dir;   # 1) Read directory entries
        closedir $dir;
        my $latest = pop @files;
        $latest ? (stat $latest)[7] : undef; # "return"
    };
    if ($old_size) {
        my $size_diff = $new_size - $old_size;
        if ( abs($size_diff) > $webcam->{max_file_size_diff} ) {
            warn "Warning: filesize of cam $cam at $self->{start} differs by: $size_diff";
        }
        else {
            print "File from $cam at $self->{start} is now $new_size (was: $old_size)\n" if $self->{debug};
        }
    }
}

=head2 process_image

Takes the downloaded image blob, and uses L<Cammy::ImageProcessor> to
add a blur over part of the image, add the current weather across the
top, and add a logo. Also creates a thumbnail image.

=cut

sub process_image {
    my $self        = shift;
    my $cam         = shift;
    my $image_blob  = shift;
    my $webcam      = $self->{config}->{webcams}->{$cam};

    my $image_out = File::Spec->catfile($webcam->{out_dir}, "$self->{start}.jpg");
    my $thumb_out = File::Spec->catfile($self->{config}->{out_dir}, "${cam}_current_thumb.jpg");

    my $ip = Cammy::ImageProcessor->new(
        image_blob  => $image_blob,
        output_file => $image_out,
        thumb_file  => $thumb_out,
        blur_radius => $webcam->{blur_radius},
        blur_sigma  => $webcam->{blur_sigma},
        shape_spec  => $webcam->{shape},
    );
    if (!$ip){
        warn "Failed to read image from $cam at " . time;
        return;
    } # We don't care why it failed, just keep going
    else {
        # Process the image
        $ip->blur_mask();
        $ip->label( $webcam->{weather}, $webcam->{warnings} );
        $ip->add_banner($self->{config}->{logo});
        $ip->thumb($webcam->{thumb_geo});
        if (my $errors = $ip->write()) {
            warn "$errors"; # Make sure that it wrote out correctly, if not throw an error
        }

        # Create the "current.jpg" symlink
        $self->create_symlink($cam, $image_out);
    }
}

=head2 create_symlink

Creates a "current" symlink so you can easily link to the latest image.

Uses L<File::Symlink::Atomic> to give the symlink a new target in an
atomic fashion, if it already exists.

=cut

sub create_symlink {
    my $self            = shift;
    my $cam             = shift;
    my $symlink_target  = shift;

    my $symlink_name   = File::Spec->catfile($self->{config}->{out_dir}, "${cam}_current.jpg");
    print "Linking $symlink_name -> $symlink_target\n" if $self->{debug};

    # Atomic, thanks to File::Symlink::Atomic
    symlink $symlink_target, $symlink_name
        or warn "Couldn't create the 'current' symlink for $cam: $!";
}


=head2 handle_one_cam

Fetch one webcam image and process it.

=cut

sub handle_one_cam {
    my $self   = shift;
    my $cam    = shift;

    {
        my $image_blob = $self->fetch_image($cam);

        # Check for a spike in filesize and warn the user about it
        $self->check_filesize($cam, length $image_blob);

        $self->process_image($cam, $image_blob);
    }

    if ($self->{debug}) {
        my $time = DateTime->now( time_zone => $self->{tz} );
        print "Worker PID $$ finished at $time, and will exit in 60s\n";
        sleep 60;
    }
}

1;

=head1 FILES

=over 4

=item F<webcam.conf>

contains a OPTION=VALUE formated list supporting the following options:
    logo (Default: logo.png)- The image file used as the logo

    blurRadius (Default: 5)-These are used in the blurring process by ImageMagick
    blurSigma  (Default: 8) see http://www.imagemagick.org/Usage/blur

    interval (Default: 30)-Interval in seconds that images are pulled
    cron_interval (Default: 1)-Interval that the script will be run (images will be pulled for this length)

    webcam_file (Default: webcams.list) -File that contains a list of webcams. See Webcam List.
    maxFileSizeDiff (Default: 1024) - Minimum file size difference (in bytes)
                                      between two images from the same source that will trigger a warning

=item F<webcams.list>

A file with a list of webcams formated as NAME URL SHAPEFILE WEATHER_REGION

=item F<webcam.shape>
-Use the format PRIMATIVE X,Y... to define a region that is cropped or blurred, this is the format used by imagemagick to define geometry.
  For example a 100x200 rectangle placed in the top left corner would be defined as follows:
    rectangle 0,0 100,200
  For more examples of supported primitives see:
    http://www.imagemagick.org/Usage/draw/

=item F<WEATHER_REGION.weather>

Contains the weather data gathered from L<Cammy::Fetch::Weather>.

=item F<WEATHER_REGION.warnings>

Contains weather warnings(if any) from L<Cammy::Fetch::Weather>.

=back

=cut
